#include "ftpsessionworker.h"
#include <QDir>
#include <QFileInfoList>
#include <QFileInfo>
#include <QDateTime>

FtpSessionWorker::FtpSessionWorker(qintptr socketDescriptor, const ServerConfig &serverConfig, QObject *parent) :
    QObject(parent),
    m_socketDescriptor(socketDescriptor),
    m_serverConfig(serverConfig)
{

}

FtpSessionWorker::~FtpSessionWorker()
{

}

void FtpSessionWorker::init()
{
    m_cmdSocket = new QTcpSocket;
    m_dataSocket = new QTcpSocket;
    m_dataServer = new QTcpServer;

    connect(m_cmdSocket, &QTcpSocket::readyRead, this, &FtpSessionWorker::onReadyRead);
    connect(m_cmdSocket, &QTcpSocket::errorOccurred, this, &FtpSessionWorker::onError);
    connect(m_cmdSocket, &QTcpSocket::disconnected, this, &FtpSessionWorker::onDisconnected);

    m_cmdSocket->setSocketDescriptor(m_socketDescriptor);
    m_cmdSocket->write("220 Welcome to Simple FTP Server\r\n");
    m_cmdSocket->flush();
}

void FtpSessionWorker::clean()
{
    delete m_cmdSocket;
    delete m_dataSocket;
    delete m_dataServer;
}

void FtpSessionWorker::onReadyRead()
{
    while (m_cmdSocket->canReadLine())
        m_commandQueue.append(FtpCommand(m_cmdSocket->readLine()));

    startNextCommand();
}

void FtpSessionWorker::onError(QAbstractSocket::SocketError error)
{
    qDebug() << "error" << error << m_cmdSocket->errorString();
}

void FtpSessionWorker::onDisconnected()
{
    qDebug() << "disconnected";
    emit quit();
}

void FtpSessionWorker::startNextCommand()
{
    if (!m_currentCommand.isNull() || m_commandQueue.isEmpty())
        return;

    m_currentCommand = m_commandQueue.takeFirst();
    QByteArray cmd = m_currentCommand.cmd();
    if (cmd == "USER")
        handleUSER();
    else if (cmd == "PASS")
        handlePASS();
    else if (cmd == "FEAT")
        handleFEAT();
    else if (cmd == "OPTS")
        handleOPTS();
    else if (cmd == "PORT")
        handlePORT();
    else if (cmd == "PASV")
        handlePASV();
    else if (cmd == "TYPE")
        handleTYPE();
    else if (cmd == "STRU")
        handleSTRU();
    else if (cmd == "MODE")
        handleMODE();
    else if (cmd == "CWD" || cmd == "XCWD")
        handleCWD();
    else if (cmd == "CDUP")
        handleCDUP();
    else if (cmd == "QUIT")
        handleQUIT();
    else if (cmd == "DELE")
        handleDELE();
    else if (cmd == "RMD" || cmd == "XRMD")
        handleRMD();
    else if (cmd == "MKD" || cmd == "XMKD")
        handleMKD();
    else if (cmd == "PWD" || cmd == "XPWD")
        handlePWD();
    else if (cmd == "RNFR")
        handleRNFR();
    else if (cmd == "RNTO")
        handleRNTO();
    else if (cmd == "LIST")
        handleLIST();
    else if (cmd == "NLST")
        handleNLST();
    else if (cmd == "RETR")
        handleRETR();
    else if (cmd == "STOR")
        handleSTOR();
    else if (cmd == "STOU")
        handleSTOU();
    else if (cmd == "APPE")
        handleAPPE();
    else if (cmd == "ALLO")
        handleALLO();
    else if (cmd == "REST")
        handleREST();
    else if (cmd == "ABOR")
        handleABOR();
    else if (cmd == "SYST")
        handleSYST();
    else if (cmd == "STAT")
        handleSTAT();
    else if (cmd == "HELP")
        handleHELP();
    else if (cmd == "NOOP")
        m_cmdSocket->write("200 Command okay.\r\n");
    else
        m_cmdSocket->write("502 Command not implemented.\r\n");

    m_cmdSocket->flush();

    m_lastCommand = m_currentCommand;
    m_currentCommand.setNull();

    if (!m_commandQueue.isEmpty())
        QMetaObject::invokeMethod(this, "startNextCommand", Qt::QueuedConnection);
}

void FtpSessionWorker::handleUSER()
{
    QByteArray arg = m_currentCommand.arg().toLower();
    if (arg == "ftp" || arg == "anonymous") {
        m_isAnonymous = true;
        m_cmdSocket->write("331 Anonymous user okay, need password <e-mail>.\r\n");
    } else {
        m_isAnonymous = false;
        m_cmdSocket->write("331 User name okay, need password.\r\n");
    }
}

void FtpSessionWorker::handlePASS()
{
    if (m_lastCommand.cmd() != "USER") {
        m_cmdSocket->write("503 Login with USER first.\r\n");
    } else {
        if (m_isAnonymous
                || m_serverConfig.verifyUser(m_lastCommand.arg(), m_currentCommand.arg())) {
            m_cmdSocket->write("230 User logged in.\r\n");
        } else {
            m_cmdSocket->write("530 Not logged in.\r\n");
        }
    }
}

void FtpSessionWorker::handleFEAT()
{
    m_cmdSocket->write("211-Extensions supported:\r\n");
    m_cmdSocket->write("UTF8\r\n");
    m_cmdSocket->write("211 End.\r\n");
}

void FtpSessionWorker::handleOPTS()
{
    if (m_currentCommand.arg() == "UTF8 ON") {
        m_cmdSocket->write("200 Command okay.\r\n");
    } else {
        m_cmdSocket->write("502 Command not implemented.\r\n");
    }
}

void FtpSessionWorker::handlePORT()
{
    // 主动模式，服务端主动连接
    // PORT h1,h2,h3,h4,p1,p2
    QStringList parts = QString::fromUtf8(m_currentCommand.arg()).split(',');
    if (parts.size() < 6) {
        m_cmdSocket->write("500 Illegal PORT command.\r\n");
        return;
    }

    QString ip = parts.mid(0, 4).join('.');
    quint16 port = (parts[4].toInt() << 8) + parts[5].toInt();
    m_dataSocket->close();
    m_dataSocket->connectToHost(ip, port);
    m_dataSocket->waitForConnected();
    m_cmdSocket->write("200 PORT command successful.");
}

void FtpSessionWorker::handlePASV()
{
    // 被动模式，服务端等待连接
    m_dataServer->close();
    m_dataSocket->close();

    QHostAddress hostAddress = m_cmdSocket->localAddress();
    m_dataServer->listen(hostAddress);

    QString ip = hostAddress.toString();
    quint16 port = m_dataServer->serverPort();

    qDebug() << "Server listen on:" << ip.toUtf8().data() << ":" << port;

    QStringList parts = ip.split(".");
    if (parts.size() < 4) {
        m_cmdSocket->write("421 Error.\r\n");
    } else {
        QString msg("227 Entering Passive Mode (%1,%2,%3,%4,%5,%6).\r\n");
        msg = msg.arg(parts[0]).arg(parts[1]).arg(parts[2]).arg(parts[3]).arg(port >> 8).arg(port & 255);
        m_cmdSocket->write(msg.toUtf8());
        qDebug() << "PASV response:" << msg;
    }
    m_cmdSocket->flush();

    m_dataServer->waitForNewConnection(-1);
    QTcpSocket *tcpSocket = m_dataServer->nextPendingConnection();
    if (!tcpSocket) {
        m_dataServer->close();
        return;
    }

    if (m_dataSocket) {
        delete m_dataSocket;
        m_dataSocket = nullptr;
    }

    m_dataSocket = tcpSocket;
    m_dataServer->close();
}

void FtpSessionWorker::handleTYPE()
{
    QByteArray arg = m_currentCommand.arg().toUpper();
    if (arg == "I" || arg == "L8" || arg == "L 8") {
        m_isAsciiMode = false;
        m_cmdSocket->write("200 Switching to Binary mode.\r\n");
    } else if (arg == "A" || arg == "A N") {
        m_isAsciiMode = true;
        m_cmdSocket->write("200 Switching to ASCII mode.\r\n");
    } else {
        m_cmdSocket->write("500 Unrecognised TYPE command.\r\n");
    }
}

void FtpSessionWorker::handleSTRU()
{
    QByteArray arg = m_currentCommand.arg().toUpper();
    if (arg == "F") {
        m_cmdSocket->write("200 Structure set to F.\r\n");
    } else {
        m_cmdSocket->write("504 Bad STRU command.\r\n");
    }
}

void FtpSessionWorker::handleMODE()
{
    QByteArray arg = m_currentCommand.arg().toUpper();
    if (arg == "S") {
        m_cmdSocket->write("200 Mode set to S.\r\n");
    } else {
        m_cmdSocket->write("504 Bad MODE command.\r\n");
    }
}

void FtpSessionWorker::handleCWD()
{
    QByteArray arg = m_currentCommand.arg();

    QDir dir(resolvePathName(arg));
    if (dir.exists() && isInRootDir(dir.absolutePath())) {
        m_currentDir = pathFromRoot(dir.absolutePath());
        m_cmdSocket->write("250 Directory successfully changed.\r\n");
    } else {
        m_cmdSocket->write("550 Failed to change directory.\r\n");
    }
}

void FtpSessionWorker::handleCDUP()
{
    QDir dir(m_currentDir + "/..");
    if (dir.exists() && isInRootDir(dir.absolutePath())) {
        m_currentDir = pathFromRoot(dir.absolutePath());
        m_cmdSocket->write("250 Directory successfully changed.\r\n");
    } else {
        m_cmdSocket->write("550 Failed to change directory.\r\n");
    }
}

void FtpSessionWorker::handleQUIT()
{
    m_cmdSocket->write("221 Goodbye.\r\n");
    m_cmdSocket->flush();
    m_cmdSocket->close();
    m_dataSocket->close();
    m_cmdSocket->waitForDisconnected();
    m_dataSocket->waitForDisconnected();
}

void FtpSessionWorker::handleDELE()
{
    QByteArray arg = m_currentCommand.arg();
    QString pathName = resolvePathName(arg);

    bool ok(false);

    QFileInfo fileInfo(pathName);
    if (fileInfo.exists() && fileInfo.isFile() && isInRootDir(fileInfo.absoluteFilePath())) {
        ok = QFile::remove(fileInfo.absoluteFilePath());
    }

    if (ok) {
        m_cmdSocket->write("250 Delete operation successful.\r\n");
    } else {
        m_cmdSocket->write("550 Delete operation failed.\r\n");
    }
}

void FtpSessionWorker::handleRMD()
{
    QByteArray arg = m_currentCommand.arg();
    QString pathName = resolvePathName(arg);

    bool ok(false);

    QFileInfo fileInfo(pathName);
    if (fileInfo.exists() && fileInfo.isDir()) {
        QDir dir(pathName);
        QString absolutePath = dir.absolutePath();
        if (isInRootDir(absolutePath) && absolutePath != m_serverConfig.rootDirectory()) {
            ok = dir.removeRecursively();
        }
    }

    if (ok) {
        m_cmdSocket->write("250 Remove directory operation successful.\r\n");
    } else {
        m_cmdSocket->write("550 Remove directory operation failed.\r\n");
    }
}

void FtpSessionWorker::handleMKD()
{
    QByteArray arg = m_currentCommand.arg();
    QDir dir(resolvePathName(arg));
    if (isInRootDir(dir.absolutePath())) {
        if (dir.mkpath(".")) {
            QString path = pathFromRoot(dir.absolutePath());
            m_cmdSocket->write(QString("257 \"%1\" created\r\n").arg(path).toUtf8());
            return;
        }
    }

    m_cmdSocket->write("550 Create directory operation failed.\r\n");
}

void FtpSessionWorker::handlePWD()
{
    m_cmdSocket->write(QString("257 \"%1\" is the current directory\r\n").arg(m_currentDir).toUtf8());
}

void FtpSessionWorker::handleRNFR()
{
    QByteArray arg = m_currentCommand.arg();
    QFileInfo path = resolvePathName(arg);
    if (path.exists() && isInRootDir(path.absoluteFilePath())) {
        m_cmdSocket->write("350 Ready for RNTO.\r\n");
        m_currentCommand.setData(path.absoluteFilePath());
        return;
    }

    m_cmdSocket->write("550 RNFR command failed.\r\n");
}

void FtpSessionWorker::handleRNTO()
{
    bool ok(false);
    if (m_lastCommand.cmd() == "RNFR" && !m_lastCommand.data().isEmpty()) {
        QByteArray arg = m_currentCommand.arg();
        QFileInfo path = resolvePathName(arg);
        if (isInRootDir(path.absoluteFilePath())) {
            ok = QFile::rename(m_lastCommand.data(), path.absoluteFilePath());
        }
    }

    if (ok) {
        m_cmdSocket->write("250 Rename successful.\r\n");
    } else {
        m_cmdSocket->write("550 Rename failed.\r\n");
    }
}

void FtpSessionWorker::handleLIST(bool detail)
{
    if (!m_dataSocket->isValid()) {
        m_cmdSocket->write("425 Can't open data connection.\r\n");
    } else {
        m_cmdSocket->write("125 Data connection already open; transfer starting.\r\n");

        QByteArray arg = m_currentCommand.arg();
        if (arg.isEmpty()) {
            arg = ".";
        }

        QFileInfo path = resolvePathName(arg);

        qDebug() << "---" << path.absoluteFilePath();

        QString listData(getListData(path.absoluteFilePath(), detail));
        m_dataSocket->write(listData.toUtf8());
        m_dataSocket->flush();
        m_dataSocket->close();
        m_cmdSocket->write("226 Directory send OK.\r\n");
    }
}

void FtpSessionWorker::handleNLST()
{
    handleLIST(false);
}

void FtpSessionWorker::handleRETR()
{
    qint64 offset = m_restartPos;
    m_restartPos = 0;

    if (!m_dataSocket->isValid()) {
        m_cmdSocket->write("425 Can't open data connection.\r\n");
        return;
    }

    QByteArray arg = m_currentCommand.arg();
    QFileInfo fileInfo(resolvePathName(arg));
    if (!fileInfo.exists() || !isInRootDir(fileInfo.absoluteFilePath())) {
        m_cmdSocket->write("451 Failure reading local file.\r\n");
        m_dataSocket->close();
        return;
    }

    QFile file(fileInfo.absoluteFilePath());

    QIODevice::OpenMode openMode = QIODevice::ReadOnly;
    if (m_isAsciiMode) {
        openMode |= QIODevice::Text;
    }

    if (!file.open(openMode)) {
        m_cmdSocket->write("451 Failure reading local file.\r\n");
        m_dataSocket->close();
        return;
    }

    if (offset != 0) {
        file.seek(offset);
    }

    while (!file.atEnd()) {
        QByteArray byteArray = file.read(1024);
        if (byteArray.isEmpty()) {
            break;
        }

        qint64 ret = m_dataSocket->write(byteArray);
        if (ret != byteArray.size()) {
            m_cmdSocket->write("451 Failure reading local file.\r\n");
            m_dataSocket->close();
            return;
        }

        m_dataSocket->flush();
        m_dataSocket->waitForBytesWritten();
    }

    m_cmdSocket->write("226 Transfer complete.\r\n");
    m_dataSocket->close();
}

void FtpSessionWorker::handleSTOR()
{
    qint64 offset = m_restartPos;
    m_restartPos = 0;

    if (!m_dataSocket->isValid()) {
        m_cmdSocket->write("425 Can't open data connection.\r\n");
        return;
    }

    QByteArray arg = m_currentCommand.arg();
    QFileInfo fileInfo(resolvePathName(arg));
    if (!isInRootDir(fileInfo.absoluteFilePath())) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    QFile file(fileInfo.absoluteFilePath());

    QIODevice::OpenMode openMode = QIODevice::WriteOnly;
    if (m_isAsciiMode) {
        openMode |= QIODevice::Text;
    }

    if (!file.open(openMode)) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    if (offset != 0) {
        file.seek(offset);
    }

    while (m_dataSocket->isValid()) {
        QByteArray byteArray = m_dataSocket->readAll();
        qint64 ret = file.write(byteArray);
        if (ret != byteArray.size()) {
            m_cmdSocket->write("451 Failure reading local file.\r\n");
            m_dataSocket->close();
            return;
        }

        file.flush();
    }

    file.close();
    m_cmdSocket->write("226 Transfer complete.\r\n");
    m_dataSocket->close();
}

void FtpSessionWorker::handleSTOU()
{
    m_restartPos = 0;

    if (!m_dataSocket->isValid()) {
        m_cmdSocket->write("425 Can't open data connection.\r\n");
        return;
    }

    QByteArray arg = m_currentCommand.arg();
    QFileInfo fileInfo(resolvePathName(arg));
    if (!isInRootDir(fileInfo.absoluteFilePath())) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    int num(1);
    while (fileInfo.exists()) {
        fileInfo = QFileInfo(fileInfo.absoluteFilePath() + "." + QString::number(num++));
    }

    QFile file(fileInfo.absoluteFilePath());

    QIODevice::OpenMode openMode = QIODevice::WriteOnly;
    if (m_isAsciiMode) {
        openMode |= QIODevice::Text;
    }

    if (!file.open(openMode)) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    while (m_dataSocket->isValid()) {
        QByteArray byteArray = m_dataSocket->readAll();
        qint64 ret = file.write(byteArray);
        if (ret != byteArray.size()) {
            m_cmdSocket->write("451 Failure reading local file.\r\n");
            m_dataSocket->close();
            return;
        }

        file.flush();
    }

    file.close();
    m_cmdSocket->write("226 Transfer complete.\r\n");
    m_dataSocket->close();
}

void FtpSessionWorker::handleAPPE()
{
    m_restartPos = 0;

    if (!m_dataSocket->isValid()) {
        m_cmdSocket->write("425 Can't open data connection.\r\n");
        return;
    }

    QByteArray arg = m_currentCommand.arg();
    QFileInfo fileInfo(resolvePathName(arg));
    if (!isInRootDir(fileInfo.absoluteFilePath())) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    QFile file(fileInfo.absoluteFilePath());

    QIODevice::OpenMode openMode = QIODevice::WriteOnly | QIODevice::Append;
    if (m_isAsciiMode) {
        openMode |= QIODevice::Text;
    }

    if (!file.open(openMode)) {
        m_cmdSocket->write("451 Failure writing to local file.\r\n");
        m_dataSocket->close();
        return;
    }

    while (m_dataSocket->isValid()) {
        QByteArray byteArray = m_dataSocket->readAll();
        qint64 ret = file.write(byteArray);
        if (ret != byteArray.size()) {
            m_cmdSocket->write("451 Failure reading local file.\r\n");
            m_dataSocket->close();
            return;
        }

        file.flush();
    }

    file.close();
    m_cmdSocket->write("226 Transfer complete.\r\n");
    m_dataSocket->close();
}

void FtpSessionWorker::handleALLO()
{
    m_cmdSocket->write("202 ALLO command ignored.\r\n");
}

void FtpSessionWorker::handleREST()
{
    m_restartPos = m_currentCommand.arg().toLongLong();
    if (m_restartPos < 0) {
        m_restartPos = 0;
    }

    m_cmdSocket->write(QString("350 Restart position accepted (%1).\r\n").arg(m_restartPos).toUtf8());
}

void FtpSessionWorker::handleABOR()
{
    m_cmdSocket->write("225 No transfer to ABOR.\r\n");
}

void FtpSessionWorker::handleSYST()
{
    m_cmdSocket->write("215 UNIX Type: L8\r\n");
}

void FtpSessionWorker::handleSTAT()
{
    QByteArray arg = m_currentCommand.arg();
    if (arg.isEmpty()) {
        m_cmdSocket->write("211 STAT command ignored.\r\n");
    } else {
        m_cmdSocket->write("213-STAT\r\n");

        QFileInfo path = resolvePathName(arg);
        QString listData(getListData(path.absoluteFilePath(), true));
        const QStringList rows = listData.split("\r\n");
        for (const QString &row : rows) {
            m_cmdSocket->write((row.trimmed() + "\r\n").toUtf8());
        }

        m_cmdSocket->write("213 End.\r\n");
    }
}

void FtpSessionWorker::handleHELP()
{
    m_cmdSocket->write("214-Help\r\n");
    m_cmdSocket->write("214 Help OK.\r\n");
}

QString FtpSessionWorker::resolvePathName(const QString &pathName)
{
    QString result;
    if (pathName.startsWith("/")) {
        result = m_serverConfig.rootDirectory() + pathName;
    } else {
        result = m_serverConfig.rootDirectory() + m_currentDir + "/" + pathName;
    }

    return result;
}

QString FtpSessionWorker::pathFromRoot(const QString &pathName)
{
    if (pathName.contains(m_serverConfig.rootDirectory())) {
        QString path = pathName.right(pathName.size() - m_serverConfig.rootDirectory().size());
        if (!path.startsWith("/")) {
            return "/" + path;
        }
    }

    return pathName;
}

QString FtpSessionWorker::getListData(const QString &path, bool detail)
{
    QString result("\r\n");

    QDir dir(path);
    if (!dir.exists()) {
        return result;
    }

    if (detail) {
        result.clear();
        int currentYear = QDate::currentDate().year();

        const QFileInfoList entryInfoList = dir.entryInfoList(QDir::Files | QDir::Dirs);
        for (const QFileInfo &entryInfo : entryInfoList) {
            QString fileInfoStr;
            fileInfoStr = entryInfo.isFile() ? "-" : "d";
            fileInfoStr += "rwxrwxrwx 1 root root " + QString::number(entryInfo.size());

            QDateTime dateTime = entryInfo.lastModified();
            fileInfoStr += " " + dateTime.toString("MMM d ");

            if (dateTime.date().year() == currentYear) {
                fileInfoStr += dateTime.toString("hh:mm");
            } else {
                fileInfoStr += dateTime.toString("yyyy");
            }

            fileInfoStr += " " + entryInfo.fileName() + "\r\n";
            result += fileInfoStr;
        }
    } else {
        QStringList names = dir.entryList(QDir::Files | QDir::Dirs);
        result = names.join("\r\n") + "\r\n";
    }

    return result;
}

bool FtpSessionWorker::isInRootDir(const QString &path)
{
    return path.contains(m_serverConfig.rootDirectory());
}


FtpCommand::FtpCommand(const QByteArray &byteArray)
{
    QByteArray str = byteArray.trimmed();
    QList<QByteArray> parts = str.split(' ');
    if (parts.size() > 0) {
        m_cmd = parts.takeFirst().toUpper();
        if (parts.size() > 0)
            m_arg = parts.join(' ').trimmed();
    }
}

QByteArray FtpCommand::cmd() const
{
    return m_cmd;
}

QByteArray FtpCommand::arg() const
{
    return m_arg;
}

QString FtpCommand::data() const
{
    return m_data;
}

void FtpCommand::setData(const QString &data)
{
    m_data = data;
}

bool FtpCommand::isNull() const
{
    return m_cmd.isEmpty();
}

void FtpCommand::setNull()
{
    m_cmd.clear();
    m_arg.clear();
}
